
import re
import datetime

MAX_OFFSET = 1440

DT_RX = re.compile(
    r'^(?P<year>[0-9]{4})'                          # YYYY
    r'(-(?P<month>[0-9]{2})'                        # -MM
    r'(-(?P<day>[0-9]{2})'                          # -DD
    r'([T: ]'                                       # 'T', ':' or ' '
    r'(?P<hour>[0-9]{2}):(?P<minute>[0-9]{2})'      # HH:MM
    r'(:(?P<second>[0-9]{2})'                       # :SS
    r'(\.(?P<fraction>[0-9]+))?)?'                  # .s...
    r'(?P<tzstring>(?P<tzutc>Z)'                    # 'Z'
    r'|(?P<tzsign>[-+])'                            # '+' or '-'
    r'(?P<tzhour>[0-9]{2}):(?P<tzminute>[0-9]{2})'  # ZZ:ZZ
    r')?'       # close optional tzstring
    r')?'       # close optional time
    r')?'       # close optional day
    r')?$'      # close optional month
)


_now = datetime.datetime.now
_utcnow = datetime.datetime.utcnow


def reset():
    global _now, _utcnow
    _now = datetime.datetime.now
    _utcnow = datetime.datetime.utcnow


def timetravel(now, freeze=False):
    global _now, _utcnow
    utc = now.astimezone(UTC)

    reset()
    if freeze:
        print 'Time frozen at', now
        _now = lambda t=now.replace(tzinfo=None):t
        _utcnow = lambda t=utc.replace(tzinfo=None):t
    else:
        print 'Time travel to', now
        offset = utcnow0() - utc
        from datetime import datetime
        _now = lambda f=datetime.now,o=offset:f()-o
        _utcnow = lambda f=datetime.utcnow,o=offset:f()-o


def parse(string, tzinfo=None):
    tup = parse_tuple(string)
    if tup[-1] is not None:
        tz = (tzinfo or FixedOffset)(tup[-1])
    else:
        tz = None
    return datetime.datetime(*tup[:-1], **{'tzinfo':tz})


def parse_tuple(string):
    m = DT_RX.match(string)
    if not m:
        raise Invalid('Invalid timestamp: \'%s\'' % string)
    m = m.groupdict()
    year = int(m['year'])
    month = int(m['month'] or 1)
    day = int(m['day'] or 1)
    hour = int(m['hour'] or 0)
    minute = int(m['minute'] or 0)
    second = int(m['second'] or 0)
    fraction = int(float('0.%s' % (m['fraction'] or 0)) * 1000000)
    zs = m['tzsign']
    zh = int(m['tzhour'] or 0)
    zm = int(m['tzminute'] or 0)
    if m['tzutc']:
        offset = 0
    elif m['tzstring']:
        zh = zs == '-' and -zh or zh
        zm = zs == '-' and -zm or zm
        offset = zh * 60 + zm
    else:
        offset = None
    if offset and abs(offset) >= MAX_OFFSET:
        raise Invalid('timezone offset is too large', offset)
    return (year, month, day,
            hour, minute, second,
            fraction, offset)


def _mkname(m):
    s = '-+'[abs(m) == m]
    h = abs(m) / 60
    m = abs(m) % 60
    return 'UTC%s%02d:%02d' % (s, h, m)



UTC = None



class Invalid(Exception):
    '''Raised when there is a problem parsing a date string'''


class FixedOffset(datetime.tzinfo):

    def __new__(self, minutes, name=None):
        if UTC is not None and minutes == 0:
            return UTC
        return datetime.tzinfo.__new__(self, minutes, name)

    def __init__(self, minutes, name=None):
        assert minutes is not None
        self.minutes = minutes
        self.name = name or _mkname(self.minutes)
        self.offset = datetime.timedelta(minutes=minutes)

    def __reduce__(self):
        return FixedOffset, (self.minutes, self.name)

    def __str__(self):
        return self.name

    def __repr__(self):
        return '<FixedOffset %s>' % self.name

    def tzname(self, dt=None):
        return self.name

    def utcoffset(self, dt):
        return self.offset

    def dst(self, dt):
        return datetime.timedelta(0)

    def localize(self, dt):
        assert not dt.tzinfo
        return dt.replace(tzinfo=self)

    def normalize(self, dt):
        assert tzinfo
        return dt.replace(tzinfo=self)

    def now(self):
        if self.minutes:
            return UTC.now().astimezone(self)
        else:
            return self.localize(_utcnow())

    def now0(self):
        return self.now().replace(microsecond=0)

    def now00(self):
        return self.now().replace(microsecond=0, second=0)



UTC = FixedOffset(0)
now = lambda:_now()
utcnow = UTC.now
utcnow0 = UTC.now0
utcnow00 = UTC.now00
maxz = UTC.localize(datetime.datetime.max)
minz = UTC.localize(datetime.datetime.min)
today = lambda tz: utcnow().astimezone(tz).date()
localnow = lambda tz: utcnow().astimezone(tz)
localnow0 = lambda tz: utcnow0().astimezone(tz)
localnow00 = lambda tz: utcnow00().astimezone(tz)


def test():
    valid = '''
    2000-01-01T00:00:00.000000 = 2000
    2000-01-01T00:00:00.000000 = 2000-01
    2000-01-02T00:00:00.000000 = 2000-01-02
    2000-01-02T03:04:00.000000 = 2000-01-02T03:04
    2000-01-02T03:04:05.000000 = 2000-01-02 03:04:05
    2000-01-02T03:04:05.600000 = 2000-01-02T03:04:05.6
    2000-01-02T03:04:05.670000 = 2000-01-02 03:04:05.67
    2000-01-02T03:04:05.678000 = 2000-01-02T03:04:05.678
    2000-01-02T03:04:05.678900 = 2000-01-02T03:04:05.6789
    2000-01-02T03:04:00.000000+0000 = 2000-01-02T03:04Z
    2000-01-02T03:04:05.000000+0000 = 2000-01-02 03:04:05Z
    2000-01-02T03:04:05.600000+0000 = 2000-01-02T03:04:05.6Z
    2000-01-02T03:04:05.670000+0000 = 2000-01-02 03:04:05.67Z
    2000-01-02T03:04:05.678000+0000 = 2000-01-02T03:04:05.678Z
    2000-01-02T03:04:00.000000+0900 = 2000-01-02T03:04+09:00
    2000-01-02T03:04:05.000000-0930 = 2000-01-02 03:04:05-09:30
    2000-01-02T03:04:05.600000+1000 = 2000-01-02T03:04:05.6+10:00
    2000-01-02T03:04:05.670000-1030 = 2000-01-02 03:04:05.67-10:30
    2000-01-02T03:04:05.678000+1100 = 2000-01-02T03:04:05.678+11:00
    '''

    invalid = '''
    2000-
    2000-1
    2000-1-2
    2000-1-2T3:04
    2000-1-2T03:04
    2000-01-02T3:04
    2000-01-02T3:04
    2000-01-02T0304
    2000-01-02T0345
    2000-01-02T03456
    2000-01-02T03:04+1
    2000-01-02T03:04-10
    2000-01-02T03:04Z+10:00
    '''

    ok = True
    for s in [' '] + invalid.strip().split('\n'):
        test = s.strip()
        e = ''
        passed = False
        try:
            parsed = parse(test)
        except Invalid:
            passed = True
        except:
            passed = False
        if not passed:
            print 'FAIL:', test

    for s in valid.strip().split('\n'):
        check, test = s.strip().split(' = ')
        try:
            parsed = parse(test)
            assert check == parsed.strftime('%Y-%m-%dT%H:%M:%S.%f%z')
        except:
            ok = False
            print 'FAIL:', test

    print ok and 'PASS' or 'FAIL'


if __name__ == '__main__':
    test()

